import { Puck, FieldLabel, AutoField } from "@/puck";
import { ConfigPreview, PuckPreview } from "@/docs/components/Preview";
import { Callout } from "nextra/components";

# Custom

Implement a field with a custom UI. Extends [Base](base).

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      title: {
        type: "custom",
        render: ({ name, onChange, value }) => {
          return (
            <input
              defaultValue={value}
              name={name}
              onChange={(e) => onChange(e.currentTarget.value)}
              style={{
                background: "white",
                border: "1px solid black",
                padding: 4,
              }}
            />
          );
        },
      },
    },
    defaultProps: {
      title: "Hello, world",
    },
    render: ({ title }) => {
      return <p style={{ margin: 0 }}>{title}</p>;
    },
  }}
/>

```tsx {7-16} copy showLineNumbers
import { FieldLabel } from "@measured/puck";

const config = {
  components: {
    Example: {
      fields: {
        title: {
          type: "custom",
          render: ({ name, onChange, value }) => (
            <input
              defaultValue={value}
              name={name}
              onChange={(e) => onChange(e.currentTarget.value)}
            />
          ),
        },
      },
      render: ({ title }) => {
        return <p>{title}</p>;
      },
    },
  },
};
```

## Params

| Param                                 | Example                   | Type     | Status   |
| ------------------------------------- | ------------------------- | -------- | -------- |
| [`type`](#type)                       | `type: "custom"`          | "custom" | Required |
| [`render()`](#renderparams)           | `render: () => <input />` | Function | Required |
| [`contentEditable`](#contentEditable) | `contentEditable: true`   | Boolean  | -        |
| [`key`](#key)                         | `key: "custom-text"`      | String   | -        |

## Required params

### `type`

The type of the field. Must be `"custom"` for Custom fields.

```tsx {6} showLineNumbers copy
const config = {
  components: {
    Example: {
      fields: {
        title: {
          type: "custom",
          render: ({ name, onChange, value }) => (
            <input
              defaultValue={value}
              name={name}
              onChange={(e) => onChange(e.currentTarget.value)}
            />
          ),
        },
      },
      // ...
    },
  },
};
```

### `render(params)`

Render the custom field.

```tsx {9-14} showLineNumbers copy
import { FieldLabel } from "@measured/puck";

const config = {
  components: {
    Example: {
      fields: {
        title: {
          type: "custom",
          render: ({ name, onChange, value }) => (
            <input
              defaultValue={value}
              name={name}
              onChange={(e) => onChange(e.currentTarget.value)}
            />
          ),
        },
      },
      // ...
    },
  },
};
```

#### `params`

| Param                 | Example                    | Type     |
| --------------------- | -------------------------- | -------- |
| `field`               | `{ type: "custom" }`       | Object   |
| `id`                  | `id`                       | String   |
| `name`                | `"title"`                  | String   |
| `onChange(value, ui)` | `onChange("Hello, world")` | Function |
| `value`               | `"Hello, world"`           | Any      |

##### onChange(value, [ui])

Set the value of the field and optionally update the [Puck UI state](/docs/api-reference/data-model/app-state#ui).

| Param   | Example                       | Type                                                   | Status   |
| ------- | ----------------------------- | ------------------------------------------------------ | -------- |
| `value` | `"Hello, world"`              | Any                                                    | Required |
| `ui`    | `{leftSideBarVisible: false}` | [UiState](/docs/api-reference/data-model/app-state#ui) |          |

## Optional params

### `contentEditable`

Enable inline text editing for this field. Only works if the value is a string. Defaults to `false`.

<Callout type="warning">
  When setting `contentEditable`, your [String](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/String) prop will be converted to an [Object](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) when rendered inside [`<Puck>`](/docs/api-reference/components/puck) (but not [`<Render>`](/docs/api-reference/components/render)). When using TypeScript, change your `string` to  `string | ReactNode`.
</Callout>

```tsx {7, 10} showLineNumbers copy
const config = {
  components: {
    Example: {
      fields: {
        title: {
          type: "custom",
          contentEditable: true,
          render: ({ name, onChange, value }) => (
            <input
              value={value} // Bind to value for 2-way binding
              name={name}
              onChange={(e) => onChange(e.currentTarget.value)}
            />
          ),
        },
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      title: {
        type: "custom",
        contentEditable: true,
        render: ({ name, onChange, value }) => (
          <input
            value={value}
            name={name}
            onChange={(e) => onChange(e.currentTarget.value)}
            style={{
              background: "white",
              border: "1px solid black",
              padding: 4,
            }}
          />
        ),
      },
    },
    defaultProps: {
      title: "Edit me inline",
    },
    render: ({ title }) => {
      return <div>{title}</div>;
    },
  }}
>
  <Puck.Preview />
</ConfigPreview>

### `key`

Assign a unique key to force the field to remount when using [`resolveFields`](/docs/api-reference/configuration/component-config#resolvefieldsdata-params) to resolve different custom field types on the same prop.

```tsx {19, 28} showLineNumbers copy
const config = {
  components: {
    Example: {
      fields: {
        isText: {
          type: "radio",
          options: [
            { label: "Yes", value: true },
            { label: "No", value: false },
          ],
        },
      },
      resolveFields: (data, params) => {
        if (data.props.isText) {
          return {
            ...params.fields,
            prop: {
              type: "custom",
              key: "custom-text",
              // ...
            },
          };
        } else {
          return {
            ...params.fields,
            prop: {
              type: "custom",
              key: "custom-number",
              // ...
            },
          };
        }
      },
      // ...
    },
  },
};
```

<ConfigPreview
  label="Example"
  componentConfig={{
    fields: {
      isText: {
        type: "radio",
        options: [
          { label: "Yes", value: true },
          { label: "No", value: false },
        ],
      },
    },
    resolveFields: (data, params) => {
      if (data.props.isText) {
        return {
          ...params.fields,
          title: {
            type: "custom",
            key: "custom-text",
            render: ({ onChange, value }) => (
              <FieldLabel label="Text Input">
                  <AutoField
                    field={{ type: "text" }}
                    value={value}
                    onChange={onChange}
                  />
              </FieldLabel>
            ),
          },
        };
      } else {
        return {
          ...params.fields,
          title: {
            type: "custom",
            key: "custom-number",
            render: ({ value, onChange }) => {
              return (
                <FieldLabel label="Number Input">
                  <AutoField
                    field={{ type: "number" }}
                    value={value}
                    onChange={onChange}
                  />
                </FieldLabel>
              );
            },
          },
        };
      }
    },
    resolveData: (data, params) => {
      if (!params.changed.isText) return data;

      if (data.props.isText) {
        return {
          ...data,
          props: {
            ...data.props,
            title: "Title",
          },
        };
      } else {
        return {
          ...data,
          props: {
            ...data.props,
            title: 123,
          },
        };
      }
    },
    defaultProps: {
      isText: true,
    },
    render: ({ title }) => {
      return <p style={{color: "black"}}>{title}</p>;
    },

}}

>

  <Puck.Preview />
</ConfigPreview>

## Further reading

- [Custom Fields guide](/docs/extending-puck/custom-fields)
- [The `<FieldLabel>` API reference](/docs/api-reference/components/field-label)
